Заказчик

Приложение Procrastinate Pro+

Цель проекта

На основе данных, предоставленных компанией, необходимо провести анализ и ответить на вопросы:

  • откуда приходят пользователи и какими устройствами они пользуются;
  • сколько стоит привлечение пользователей из различных рекламных каналов;
  • сколько денег приносит каждый клиент;
  • когда расходы на привлечение клиента окупаются;
  • какие факторы мешают привлечению клиентов.

Входные данные

Данные о пользователях, привлечённых с 1 мая по 27 октября 2019 года:

лог сервера с данными об их посещениях (файл visits_info_short.csv)

User Id — уникальный идентификатор пользователя;
Region — страна пользователя;
Device — тип устройства пользователя;
Channel — идентификатор источника перехода;
Session Start — дата и время начала сессии;
Session End— дата и время окончания сессии.

выгрузка их покупок за этот период (файл orders_info_short.csv)

User Id — уникальный идентификатор пользователя;
Event Dt — дата и время покупки;
Revenue — сумма заказа.

рекламные расходы (файл costs_info_short.csv)

Channel — идентификатор рекламного источника;
Dt — дата проведения рекламной кампании;
Costs — расходы на эту кампанию.

Ход исследования

Исследование пройдёт в четыре этапа:

  • Обзор и предобработка данных;
  • Исследовательский анализ данных;
  • Анализ маркетинговых расходов;
  • Оценка окупаемости рекламы.
In [ ]:
import time
import warnings
import pandas as pd
import numpy as np
import seaborn as sns
import plotly.express as px
import matplotlib.pyplot as plt
%matplotlib inline

from datetime import datetime
from datetime import date, timedelta as td
from datetime import datetime, timedelta

pd.set_option('display.max_columns', None)
warnings.filterwarnings(action='ignore')
In [2]:
try:
    visits, orders, costs = (
        pd.read_csv('/datasets/visits_info_short.csv'),
        pd.read_csv('/datasets/orders_info_short.csv'),
        pd.read_csv('/datasets/costs_info_short.csv')
    )


except:
    visits, orders, costs = (
        pd.read_csv('visits_info_short.csv'),
        pd.read_csv('orders_info_short.csv'),
        pd.read_csv('costs_info_short.csv')
    )

РАЗВЕДОЧНЫЙ АНАЛИЗ ДАННЫХ

— Обзор датафрейма visits

In [3]:
display(visits.info(),visits.head(), visits.tail())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype 
---  ------         --------------   ----- 
 0   User Id        309901 non-null  int64 
 1   Region         309901 non-null  object
 2   Device         309901 non-null  object
 3   Channel        309901 non-null  object
 4   Session Start  309901 non-null  object
 5   Session End    309901 non-null  object
dtypes: int64(1), object(5)
memory usage: 14.2+ MB
None
User Id Region Device Channel Session Start Session End
0 981449118918 United States iPhone organic 2019-05-01 02:36:01 2019-05-01 02:45:01
1 278965908054 United States iPhone organic 2019-05-01 04:46:31 2019-05-01 04:47:35
2 590706206550 United States Mac organic 2019-05-01 14:09:25 2019-05-01 15:32:08
3 326433527971 United States Android TipTop 2019-05-01 00:29:59 2019-05-01 00:54:25
4 349773784594 United States Mac organic 2019-05-01 03:33:35 2019-05-01 03:57:40
User Id Region Device Channel Session Start Session End
309896 329994900775 UK PC LeapBob 2019-10-31 13:28:12 2019-10-31 14:39:29
309897 334903592310 France PC lambdaMediaAds 2019-10-31 22:14:52 2019-10-31 22:39:36
309898 540102010126 Germany PC organic 2019-10-31 01:40:48 2019-10-31 01:41:31
309899 308736936846 Germany Mac organic 2019-10-31 07:37:34 2019-10-31 07:37:55
309900 109329042535 Germany PC lambdaMediaAds 2019-10-31 14:17:43 2019-10-31 15:17:04

— Обзор датафрейма orders

In [4]:
display(orders.info(),orders.head(), orders.tail())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   User Id   40212 non-null  int64  
 1   Event Dt  40212 non-null  object 
 2   Revenue   40212 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 942.6+ KB
None
User Id Event Dt Revenue
0 188246423999 2019-05-01 23:09:52 4.99
1 174361394180 2019-05-01 12:24:04 4.99
2 529610067795 2019-05-01 11:34:04 4.99
3 319939546352 2019-05-01 15:34:40 4.99
4 366000285810 2019-05-01 13:59:51 4.99
User Id Event Dt Revenue
40207 651604369137 2019-10-31 16:19:07 4.99
40208 275341387049 2019-10-31 01:17:17 4.99
40209 374656616484 2019-10-31 06:17:29 4.99
40210 168548862926 2019-10-31 22:46:19 4.99
40211 329994900775 2019-10-31 13:29:06 4.99

— Обзор датафрейма costs

In [5]:
display(costs.info(), costs.head(), costs.tail())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   dt       1800 non-null   object 
 1   Channel  1800 non-null   object 
 2   costs    1800 non-null   float64
dtypes: float64(1), object(2)
memory usage: 42.3+ KB
None
dt Channel costs
0 2019-05-01 FaceBoom 113.3
1 2019-05-02 FaceBoom 78.1
2 2019-05-03 FaceBoom 85.8
3 2019-05-04 FaceBoom 136.4
4 2019-05-05 FaceBoom 122.1
dt Channel costs
1795 2019-10-23 lambdaMediaAds 4.0
1796 2019-10-24 lambdaMediaAds 6.4
1797 2019-10-25 lambdaMediaAds 8.8
1798 2019-10-26 lambdaMediaAds 8.8
1799 2019-10-27 lambdaMediaAds 12.0
In [6]:
print('Количество выявленных дубликатов в датафрейме visits:', visits.duplicated().sum())
print('Количество выявленных дубликатов в датафрейме orders:', orders.duplicated().sum())
print('Количество выявленных дубликатов в датафрейме costs:', costs.duplicated().sum())
Количество выявленных дубликатов в датафрейме visits: 0
Количество выявленных дубликатов в датафрейме orders: 0
Количество выявленных дубликатов в датафрейме costs: 0
In [7]:
display(visits.isna().sum(), '""' * 55,
        orders.isna().sum(), '""' * 55,
        costs.isna().sum())
User Id          0
Region           0
Device           0
Channel          0
Session Start    0
Session End      0
dtype: int64
'""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""'
User Id     0
Event Dt    0
Revenue     0
dtype: int64
'""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""'
dt         0
Channel    0
costs      0
dtype: int64

Промежуточный вывод:

  1. Необходимо изменить тип данных колоннок с датами на соответствующий datetime[64];
  2. Предлагаю переименовать и преобразовать к нижнему регистру колонки датафрейма и привести в соответствии со стилем snake_case;
  3. Явных дубликатов в датафреймах не выявлено, как и пропущенных значений.
In [8]:
# Переименуем колонки и преобразуем их в соответствии хорошего стиля
visits.columns = [name.lower().replace(' ', '_') for name in visits.columns]
orders.columns = [name.lower().replace(' ', '_') for name in orders.columns]
costs.columns = [name.lower().replace(' ', '_') for name in costs.columns]
In [9]:
# Изменим тип данных колонок с датами
visits.session_start = pd.to_datetime(visits.session_start)
visits.session_end = pd.to_datetime(visits.session_end)
orders.event_dt = pd.to_datetime(orders.event_dt)
costs.dt = pd.to_datetime(costs.dt).dt.date

Познакомимся с данными ближе

visits

Комментарий:
Предлагаю изучить сколько в среднем сессий приходится на одного пользователя за месяц: для этого необходимо разделить число сессий на количество пользователей за месяц - для этого создадим новые колонки с группированными значениями месяца и года.

Количество сессий соответствует числу идентификаторов пользователя.

In [10]:
visits['session_year'] = visits.session_start.dt.year
visits['session_month'] = visits.session_start.dt.month
visits.head()
Out[10]:
user_id region device channel session_start session_end session_year session_month
0 981449118918 United States iPhone organic 2019-05-01 02:36:01 2019-05-01 02:45:01 2019 5
1 278965908054 United States iPhone organic 2019-05-01 04:46:31 2019-05-01 04:47:35 2019 5
2 590706206550 United States Mac organic 2019-05-01 14:09:25 2019-05-01 15:32:08 2019 5
3 326433527971 United States Android TipTop 2019-05-01 00:29:59 2019-05-01 00:54:25 2019 5
4 349773784594 United States Mac organic 2019-05-01 03:33:35 2019-05-01 03:57:40 2019 5
In [11]:
# Найдем количество сессий и количество пользователей
sessions_per_user = visits.groupby(['session_year', 'session_month']) \
                          .agg({'user_id':['count', 'nunique']})

# Переименуем колонки
sessions_per_user.columns = ['n_sessions', 'n_users']
In [12]:
# Делим число сессий на количество пользователей
sessions_per_user['sessions_per_user'] = sessions_per_user.n_sessions / sessions_per_user.n_users
sessions_per_user.style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[12]:
    n_sessions n_users sessions_per_user
session_year session_month      
2019 5 45765 27111 1.688060
6 44623 26042 1.713501
7 47970 26672 1.798515
8 55996 31064 1.802601
9 56628 31871 1.776788
10 58919 31175 1.889944

Комментарий:

Мы видим, что количество уникальных пользователей растет в течение исследуемого периода, как и количество сессий. Но нельзя сказать, что две сессии в месяц в среднем на каждого уникального пользователя - это много для развлекательного приложения.

Посмотрим на среднюю продолжительность сессии (ASL).

In [13]:
visits['session_duration_sec'] = (visits.session_end - visits.session_start).dt.seconds
print(f'Среднее значение: {visits.session_duration_sec.mean()} секунд')
print(f'Медианное значение: {visits.session_duration_sec.median()} секунд')


# строим гистограмму
visits.session_duration_sec.hist(bins=50);
plt.title('Распределение средней продолжительности сессии')
plt.ylabel('Визиты')
plt.xlabel('Секунды')
plt.show()
Среднее значение: 1800.0908677287264 секунд
Медианное значение: 1244.0 секунд

Промежуточный вывод:
Секунды, проведённые пользователями в приложении, имеют экспоненциальное распределение с пиком в районе нуля.
И это нормально — в любом цифровом сервисе очень многие сессии заканчиваются техническими ошибками.

Средняя сессия длится в районе 30-ти минут, медианная - примерно 21 минут.

orders

In [14]:
# Просмотрим уникальные значения колонки Revenue
orders.revenue.value_counts()
Out[14]:
4.99     38631
5.99       780
9.99       385
49.99      212
19.99      204
Name: revenue, dtype: int64

Промежуточный вывод:
Как мы видим, речь идет о пяти продуктах с фиксированной ценой.

In [15]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

ax1.plot(orders.pivot_table(index = orders.event_dt.astype('datetime64[D]'), \
                              values = 'revenue', \
                              aggfunc = 'sum'))
ax1.grid(True)
ax1.set_title('Распределение выручки по дням')
ax1.set_ylabel('сумма выручки');

ax2.plot(orders.pivot_table(index = orders.event_dt.astype('datetime64[D]'), \
                              values = 'revenue', \
                              aggfunc = 'count'),color='orange')
ax2.grid(True)
ax2.set_title('Распределение количества покупок по дням')
ax2.set_ylabel('количество покупок')
plt.show();

Комментарий:
С выручкой, как и с количеством покупок в приложении, все в порядке: мы видим динамику устойчивого роста.

costs

Посмотрим на маркетинговые затраты в разрезе каналов привлечения за исследуемый период

In [16]:
costs.pivot_table(index='channel', values='costs', aggfunc='sum') \
     .sort_values(by='costs', ascending=False) \
     .reset_index() \
     .style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[16]:
  channel costs
0 TipTop 54751.300000
1 FaceBoom 32445.600000
2 WahooNetBanner 5151.000000
3 AdNonSense 3911.250000
4 OppleCreativeMedia 2151.250000
5 RocketSuperAds 1833.000000
6 LeapBob 1797.600000
7 lambdaMediaAds 1557.600000
8 MediaTornado 954.480000
9 YRabbit 944.220000

Комментарий:

Мы видим, что компания не скупится на маркетинговые расходы, имеется несколько каналов привлечения пользователей.
Основные затраты идут на рекламу в TipTop и FaceBoom.

Задайте функции для расчёта и анализа LTV, ROI, удержания и конверсии.¶

Разрешается использовать функции, с которыми вы познакомились в теоретических уроках.

Это функции для вычисления значений метрик:

  • get_profiles() — для создания профилей пользователей,
  • get_retention() — для подсчёта Retention Rate,
  • get_conversion() — для подсчёта конверсии,
  • get_ltv() — для подсчёта LTV.

А также функции для построения графиков:

  • filter_data() — для сглаживания данных,
  • plot_retention() — для построения графика Retention Rate,
  • plot_conversion() — для построения графика конверсии,
  • plot_ltv_roi — для визуализации LTV и ROI.
In [17]:
# функция для создания пользовательских профилей

def get_profiles(sessions, orders, ad_costs):

    # находим параметры первых посещений
    profiles = (
        sessions.sort_values(by=['user_id', 'session_start'])
        .groupby('user_id')
        .agg(
            {
                'session_start': 'first',
                'channel': 'first',
                'device': 'first',
                'region': 'first',
            }
        )
        .rename(columns={'session_start': 'first_ts'})
        .reset_index()
    )

    # для когортного анализа определяем дату первого посещения
    # и первый день месяца, в который это посещение произошло
    profiles['dt'] = profiles['first_ts'].dt.date
    profiles['month'] = profiles['first_ts'].astype('datetime64[M]')

    # добавляем признак платящих пользователей
    profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())

    # считаем количество уникальных пользователей
    # с одинаковыми источником и датой привлечения
    new_users = (
        profiles.groupby(['dt', 'channel'])
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'unique_users'})
        .reset_index()
    )

    # объединяем траты на рекламу и число привлечённых пользователей
    ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')

    # делим рекламные расходы на число привлечённых пользователей
    ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']

    # добавляем стоимость привлечения в профили
    profiles = profiles.merge(
        ad_costs[['dt', 'channel', 'acquisition_cost']],
        on=['dt', 'channel'],
        how='left',
    )

    # стоимость привлечения органических пользователей равна нулю
    profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)

    return profiles
In [18]:
# функция для расчёта удержания

def get_retention(
    profiles,
    sessions,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # добавляем столбец payer в передаваемый dimensions список
    dimensions = ['payer'] + dimensions

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу удержания
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # получаем таблицу динамики удержания
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time
In [19]:
# функция для расчёта конверсии

def get_conversion(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # определяем дату и время первой покупки для каждого пользователя
    first_purchases = (
        purchases.sort_values(by=['user_id', 'event_dt'])
        .groupby('user_id')
        .agg({'event_dt': 'first'})
        .reset_index()
    )

    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
    )

    # рассчитываем лайфтайм для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days

    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users' 
        dimensions = dimensions + ['cohort']

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        result = result.fillna(0).cumsum(axis = 1)
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # делим каждую «ячейку» в строке на размер когорты
        # и получаем conversion rate
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу конверсии
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # для таблицы динамики конверсии убираем 'cohort' из dimensions
    if 'cohort' in dimensions: 
        dimensions = []

    # получаем таблицу динамики конверсии
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time
In [20]:
# функция для расчёта LTV и ROI

def get_ltv(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
  
    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
    )
    
    # рассчитываем лайфтайм пользователя для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days
    
    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users'
        dimensions = dimensions + ['cohort']

    # функция группировки по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        # строим «треугольную» таблицу выручки
        result = df.pivot_table(
            index=dims, columns='lifetime', values='revenue', aggfunc='sum'
        )
        # находим сумму выручки с накоплением
        result = result.fillna(0).cumsum(axis=1)
        # вычисляем размеры когорт
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        # объединяем размеры когорт и таблицу выручки
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # считаем LTV: делим каждую «ячейку» в строке на размер когорты
        result = result.div(result['cohort_size'], axis=0)
        # исключаем все лайфтаймы, превышающие горизонт анализа
        result = result[['cohort_size'] + list(range(horizon_days))]
        # восстанавливаем размеры когорт
        result['cohort_size'] = cohort_sizes

        # собираем датафрейм с данными пользователей и значениями CAC, 
        # добавляя параметры из dimensions
        cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()

        # считаем средний CAC по параметрам из dimensions
        cac = (
            cac.groupby(dims)
            .agg({'acquisition_cost': 'mean'})
            .rename(columns={'acquisition_cost': 'cac'})
        )

        # считаем ROI: делим LTV на CAC
        roi = result.div(cac['cac'], axis=0)

        # удаляем строки с бесконечным ROI
        roi = roi[~roi['cohort_size'].isin([np.inf])]

        # восстанавливаем размеры когорт в таблице ROI
        roi['cohort_size'] = cohort_sizes

        # добавляем CAC в таблицу ROI
        roi['cac'] = cac['cac']

        # в финальной таблице оставляем размеры когорт, CAC
        # и ROI в лайфтаймы, не превышающие горизонт анализа
        roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]

        # возвращаем таблицы LTV и ROI
        return result, roi

    # получаем таблицы LTV и ROI
    result_grouped, roi_grouped = group_by_dimensions(
        result_raw, dimensions, horizon_days
    )

    # для таблиц динамики убираем 'cohort' из dimensions
    if 'cohort' in dimensions:
        dimensions = []

    # получаем таблицы динамики LTV и ROI
    result_in_time, roi_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    return (
        result_raw,  # сырые данные
        result_grouped,  # таблица LTV
        result_in_time,  # таблица динамики LTV
        roi_grouped,  # таблица ROI
        roi_in_time,  # таблица динамики ROI
    )
In [21]:
def filter_data(df, window):
    # для каждого столбца применяем скользящее среднее
    for column in df.columns.values:
        df[column] = df[column].rolling(window).mean() 
    return df

# функция для визуализации LTV и ROI

def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):

    # задаём сетку отрисовки графиков
    plt.figure(figsize=(20, 20))

    # из таблицы ltv исключаем размеры когорт
    ltv = ltv.drop(columns=['cohort_size'])
    # в таблице динамики ltv оставляем только нужный лайфтайм
    ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]

    # стоимость привлечения запишем в отдельный фрейм
    cac_history = roi_history[['cac']]

    # из таблицы roi исключаем размеры когорт и cac
    roi = roi.drop(columns=['cohort_size', 'cac'])
    # в таблице динамики roi оставляем только нужный лайфтайм
    roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
        [horizon - 1]
    ]

    # первый график — кривые ltv
    ax1 = plt.subplot(3, 2, 1)
    ltv.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('LTV')

    # второй график — динамика ltv
    ax2 = plt.subplot(3, 2, 2, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in ltv_history.index.names if name not in ['dt']]
    filtered_data = ltv_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))

    # третий график — динамика cac
    ax3 = plt.subplot(3, 2, 3, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in cac_history.index.names if name not in ['dt']]
    filtered_data = cac_history.pivot_table(
        index='dt', columns=columns, values='cac', aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика стоимости привлечения пользователей')

    # четвёртый график — кривые roi
    ax4 = plt.subplot(3, 2, 4)
    roi.T.plot(grid=True, ax=ax4)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('ROI')

    # пятый график — динамика roi
    ax5 = plt.subplot(3, 2, 5, sharey=ax4)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in roi_history.index.names if name not in ['dt']]
    filtered_data = roi_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax5)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()
In [22]:
# функция для визуализации конверси

def plot_conversion(conversion, conversion_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 5))

    # исключаем размеры когорт
    conversion = conversion.drop(columns=['cohort_size'])
    # в таблице динамики оставляем только нужный лайфтайм
    conversion_history = conversion_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # первый график — кривые конверсии
    ax1 = plt.subplot(1, 2, 1)
    conversion.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Конверсия пользователей')

    # второй график — динамика конверсии
    ax2 = plt.subplot(1, 2, 2, sharey=ax1)
    columns = [		
        # столбцами сводной таблицы станут все столбцы индекса, кроме даты
        name for name in conversion_history.index.names if name not in ['dt']
    ]
    filtered_data = conversion_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()
In [23]:
# функция для визуализации удержания

def plot_retention(retention, retention_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 10))

    # исключаем размеры когорт и удержание первого дня
    retention = retention.drop(columns=['cohort_size', 0])
    # в таблице динамики оставляем только нужный лайфтайм
    retention_history = retention_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # если в индексах таблицы удержания только payer,
    # добавляем второй признак — cohort
    if retention.index.nlevels == 1:
        retention['cohort'] = 'All users'
        retention = retention.reset_index().set_index(['cohort', 'payer'])

    # в таблице графиков — два столбца и две строки, четыре ячейки
    # в первой строим кривые удержания платящих пользователей
    ax1 = plt.subplot(2, 2, 1)
    retention.query('payer == True').droplevel('payer').T.plot(
        grid=True, ax=ax1
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание платящих пользователей')

    # во второй ячейке строим кривые удержания неплатящих
    # вертикальная ось — от графика из первой ячейки
    ax2 = plt.subplot(2, 2, 2, sharey=ax1)
    retention.query('payer == False').droplevel('payer').T.plot(
        grid=True, ax=ax2
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание неплатящих пользователей')

    # в третьей ячейке — динамика удержания платящих
    ax3 = plt.subplot(2, 2, 3)
    # получаем названия столбцов для сводной таблицы
    columns = [
        name
        for name in retention_history.index.names
        if name not in ['dt', 'payer']
    ]
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == True').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания платящих пользователей на {}-й день'.format(
            horizon
        )
    )

    # в чётвертой ячейке — динамика удержания неплатящих
    ax4 = plt.subplot(2, 2, 4, sharey=ax3)
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == False').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax4)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания неплатящих пользователей на {}-й день'.format(
            horizon
        )
    )
    
    plt.tight_layout()
    plt.show()

Исследовательский анализ данных¶

  • Составьте профили пользователей. Определите минимальную и максимальную даты привлечения пользователей.
  • Выясните, из каких стран пользователи приходят в приложение и на какую страну приходится больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих из каждой страны.
  • Узнайте, какими устройствами пользуются клиенты и какие устройства предпочитают платящие пользователи. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого устройства.
  • Изучите рекламные источники привлечения и определите каналы, из которых пришло больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого канала привлечения.

После каждого пункта сформулируйте выводы.

Комментарий:
Создадим пользовательский профиль посетителей приложения. Для этого используем функцию get_profile и передадим ей данные о посещениях, покупках и тратах на рекламу.

In [24]:
profiles = get_profiles(visits, orders, costs)
display(profiles.head(), profiles.tail())
user_id first_ts channel device region dt month payer acquisition_cost
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07 2019-05-07 20:58:57 True 1.088172
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09 2019-07-09 12:46:07 False 1.107237
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01 2019-10-01 09:58:33 False 0.000000
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22 2019-08-22 21:35:48 False 0.988235
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02 2019-10-02 00:07:44 False 0.230769
user_id first_ts channel device region dt month payer acquisition_cost
150003 999956196527 2019-09-28 08:33:02 TipTop iPhone United States 2019-09-28 2019-09-28 08:33:02 False 3.500000
150004 999975439887 2019-10-21 00:35:17 organic PC UK 2019-10-21 2019-10-21 00:35:17 False 0.000000
150005 999976332130 2019-07-23 02:57:06 TipTop iPhone United States 2019-07-23 2019-07-23 02:57:06 False 2.600000
150006 999979924135 2019-09-28 21:28:09 MediaTornado PC United States 2019-09-28 2019-09-28 21:28:09 False 0.205714
150007 999999563947 2019-10-18 19:57:25 organic iPhone United States 2019-10-18 2019-10-18 19:57:25 False 0.000000

Комментарий:
Установим момент и горизонт анализа данных. Согласно заданию, анализ необходимо провести по сосотоянию на 1 ноября 2019 годаб но т.к. наша выгрузка содержит данные по 27 октября 2019, возьмем эту дату. Также, в бизнес-плане заложено, что пользователи должны окупаться не позднее, чем через две недели после привлечения.

In [25]:
# момент анализа
observation_date = profiles.dt.max()

# горизонт анализа
analysis_horizon = 14

print(f'Минимальная дата привлечения пользователей {profiles.dt.min()}')
print(f'Максимальная дата привлечения пользователей {profiles.dt.max()}')
Минимальная дата привлечения пользователей 2019-05-01
Максимальная дата привлечения пользователей 2019-10-27

Комментарий:
Выясним, из каких стран пользователи приходят в приложение и на какую страну приходится больше всего платящих пользователей. Построим таблицу, отражающую количество пользователей и долю платящих из каждой страны и визуализируем ее.

In [26]:
country = profiles.pivot_table(index = 'region', columns = 'payer', values = 'user_id', aggfunc = 'count') \
                  .rename(columns={True: 'payer', False: 'not_payer'}) \
                  .sort_values('payer', ascending=False)
In [27]:
fig, ax = plt.subplots(figsize=(18, 3))
country[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])

ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()

country['payer_share'] = round((country.payer / (country.payer + country.not_payer) * 100), 2)
country.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[27]:
payer region not_payer payer payer_share
0 United States 93100 6902 6.900000
1 UK 16875 700 3.980000
2 France 16787 663 3.800000
3 Germany 14365 616 4.110000

Промежуточный вывод:
Однозначно видно, что подавляющее большинство привлеченных пользователей - из США. Также эта страна лидирует и по количеству платящих пользователей. Великобритания и Франция практически на одном уровне по показателям, а аутсайдером даного "рейтинга" является Германия - но при этом по доле плятящих пользователей она на втором месте.

Узнаем, какими устройствами пользуются клиенты и какие устройства предпочитают платящие пользователи. Построим таблицу, отражающую количество пользователей и долю платящих для каждого устройства и визуализируем ее

In [28]:
device = profiles.pivot_table(index = 'device', columns = 'payer', values = 'user_id', aggfunc = 'count') \
                 .rename(columns={True: 'payer', False: 'not_payer'}) \
                 .sort_values('payer', ascending=False)
In [29]:
fig, ax = plt.subplots(figsize=(18, 3))
device[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])

ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()

device['payer_share'] = round((device.payer / (device.payer + device.not_payer) * 100), 2)
device.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[29]:
payer device not_payer payer payer_share
0 iPhone 51097 3382 6.210000
1 Android 32982 2050 5.850000
2 Mac 28130 1912 6.360000
3 PC 28918 1537 5.050000

Промежуточный вывод:
Тут все также очевидно, клиенты (платящие и нет) предпочитают пользоваться продуктами Apple

Изучим рекламные источники привлечения и определим каналы, из которых пришло больше всего платящих пользователей. Построим таблицу, отражающую количество пользователей и долю платящих для каждого канала привлечения и визуализируем ее.

In [30]:
channel = profiles.pivot_table(index = 'channel', columns = 'payer', values = 'user_id', aggfunc = 'count') \
                  .rename(columns={True: 'payer', False: 'not_payer'}) \
                  .sort_values('payer', ascending=False)
In [31]:
fig, ax = plt.subplots(figsize=(18, 7))
channel[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])

ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()

channel['payer_share'] = round((channel.payer / (channel.payer + channel.not_payer) * 100), 2)
channel.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[31]:
payer channel not_payer payer payer_share
0 FaceBoom 25587 3557 12.200000
1 TipTop 17683 1878 9.600000
2 organic 55279 1160 2.060000
3 WahooNetBanner 8100 453 5.300000
4 AdNonSense 3440 440 11.340000
5 RocketSuperAds 4096 352 7.910000
6 LeapBob 8291 262 3.060000
7 OppleCreativeMedia 8372 233 2.710000
8 lambdaMediaAds 1924 225 10.470000
9 YRabbit 4147 165 3.830000
10 MediaTornado 4208 156 3.570000

Промежуточный вывод:
Больше всего пользуются приложением *органические* пользователи, правда их доля конверсия очень низкая. Два самых больших канала: TipTop, FaceBoom - они показывают весьма неплохие результаты. Также хороший поток привлеченных платящих пользователей показывают AdNonSense и RocketSuperAds.

Вывод:
Мы посмотрели на базовые значения конверсии пользователей по стране, устройству и каналу привлечения: очевидными лидерами являются пользователи из Соединенных штатов и при этом большая часть пользователей заходит с мобильных устройств (наибольшую конверсию имеют пользователи, которые пользуются продауктами Apple). Также исследованы рекламные источники привлечения и определены каналы, из которых пришло больше всего платящих пользователей, так лучший канал привлечения который показывает лучшую конверсию - это FaceBoom

Маркетинг¶

  • Посчитайте общую сумму расходов на маркетинг.
  • Выясните, как траты распределены по рекламным источникам, то есть сколько денег потратили на каждый источник.
  • Постройте визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику. Постарайтесь отразить это на одном графике.
  • Узнайте, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника. Используйте профили пользователей.

Напишите промежуточные выводы.

Посчитаем общую сумму расходов на маркетинг

In [32]:
print(f'Общая сумма затрат на рекламу {costs.costs.sum().round()}')
Общая сумма затрат на рекламу 105497.0
In [33]:
cpt = costs.pivot_table(index='channel', values='costs', aggfunc='sum').sort_values('costs', ascending=False) \
     .reset_index()
cpt['%'] = cpt.costs / cpt.costs.sum() * 100
cpt.style.background_gradient(sns.light_palette("brown", as_cmap=True))
Out[33]:
  channel costs %
0 TipTop 54751.300000 51.898295
1 FaceBoom 32445.600000 30.754910
2 WahooNetBanner 5151.000000 4.882589
3 AdNonSense 3911.250000 3.707441
4 OppleCreativeMedia 2151.250000 2.039152
5 RocketSuperAds 1833.000000 1.737485
6 LeapBob 1797.600000 1.703930
7 lambdaMediaAds 1557.600000 1.476436
8 MediaTornado 954.480000 0.904744
9 YRabbit 944.220000 0.895018

Промежуточный вывод:
Общая сумма расходов на маркетинг: 105497 y.e.
Больше всего было потрачено на маркетинг в каналах TipTop и FaceBoom. На них приходится больше 80% всех затрат.

In [34]:
fig = px.line(costs.pivot_table(index='dt', values='costs', aggfunc='sum', columns='channel'),
              template='plotly_white', title='Затраты на привлечение пользователей в разрезе каналов',
              labels=dict(dt="дата", value="затраты на привлечение в день", channel="каналы"))
fig.show()

Промежуточный вывод:
Видим уведичение расходов на маркетинг с течением времени. В конце сентября - начале октября они достигли исторического пика в 630 у.е., после чего стали немного снижаться до 400, а затем к концу октября внось стремились к 600.

In [35]:
costs.dt = pd.to_datetime(costs.dt)
costs['week'] = costs.dt.dt.week
costs['month'] = costs.dt.dt.month

plt.figure(figsize=(25, 7))

# задаем недельные расходы
report_week = costs.pivot_table(index='channel', columns='week', values='costs', aggfunc='sum'
)

# Строим динамику расходов по Неделям
report_week.T.plot(
    grid=True, xticks=list(report_week.columns.values), ax=plt.subplot(1, 2, 1)
)
plt.title('Динамика расходов по неделям')

# задаем расходы по месяцам
report_month = costs.pivot_table(index='channel', columns='month', values='costs', aggfunc='sum')


# строим изменение расходов по месяцам
report_month.T.plot(
    grid=True, xticks=list(report_month.columns.values), ax=plt.subplot(1, 2, 2)
)
plt.title('Динамика расходов по месяцам')
plt.show()

Промежуточный вывод:
Начиная с 21-й недели началось увеличение расходов в источниках TipTop и FaceBoom. Пик расходов зафиксирован на 39-й неделе в Сентябре (9-й месяц). В остальных источниках увеличения рекламного бюджета не наблюдается

Рассчитаем средний CAC на одного пользователя для всего проекта, используя профили пользователей, но без канала Organic, т.к. этот канал привлечения приложению ничего не стоит.

In [36]:
# Рассчитаем САС на одного пользователя
cac = profiles.query('channel != "organic"')[['user_id', 'acquisition_cost']] \
              .drop_duplicates().agg({'acquisition_cost':'mean'})
cac
Out[36]:
acquisition_cost    1.127481
dtype: float64
In [37]:
# Рассчитаем САС для каждого источника трафика
cac_channel = profiles.pivot_table(index='channel', values='acquisition_cost', aggfunc='mean') \
                      .sort_values(by='acquisition_cost', ascending=False) \
                      .rename(columns={'acquisition_cost':'cac'}) \
                      .reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
cac_channel
Out[37]:
  channel cac
0 TipTop 2.799003
1 FaceBoom 1.113286
2 AdNonSense 1.008054
3 lambdaMediaAds 0.724802
4 WahooNetBanner 0.602245
5 RocketSuperAds 0.412095
6 OppleCreativeMedia 0.250000
7 YRabbit 0.218975
8 MediaTornado 0.218717
9 LeapBob 0.210172
10 organic 0.000000

Промежуточный вывод:
Как вы видим, дороже всего приложению обходится пользователь, которого привлекли просредством TipTop (средняя стоимость - 2.80 за пользователя), далее идет FaceBoom и AdNonSense примерно в равной ценовой категории (1.11 и 1.01 соответственно). Органический трафик бесплатен, остальные каналы берут за пользователя от 0.21 до 0.72 у.е

Вывод:
Всего на маркетинг потрачено 105497.3 у.е.

Выяснено, как траты распределены по источникам. Больше всего трат приходитя на источник TipTop (51.9%). Следом идет источник FaceBoom(30.75%). Далее идут низкозатратные источники WahooNetBanner (4.88%), AdNonSense (3.71%), OppleCreativeMedia (2.04%). На оставшиеся источники приходится 1-2% трат.

Визуализировано изменение метрик во времени. Заметны два "активных игрока" - это TipTop и FaceBoom. Больше всего трафика закупали у TipTop, и к октябрю число трат приближалось к 630 у.е. Число трат на FaceBoom находится в районе 300. Остальные каналы привлечения обходятся довольно дешево.

Среди рекламных источников помимо TipTop и FaceBoom с июня начали расти траты на WahooNetBanner (к октябрю их число было в районе 53 у.е.). Затраты на AdNonSense, наоборот, стали снижаться с июня 2019 года (с 55 до 35). По остальным источникам траты варьируются в пределах 3-20 у.е.

Выяснено, сколько в среднем стоило привлечение одного пользователя из каждого источника.

Рассчитан средний CAC на одного пользователя для всего проекта и для каждого источника трафика. Медианная стоимость привлечения одного пользователя - 0.24. При этом среднее находится на значении 0.7. Вероятно, в какие-то периоды мы тратили на пользователей больше, и в данных есть какие-то выбросы, которые влияют на разницу в стреднем и медианой. Максимальная стоимость привлечения пользователя - 3.71 у.е.

На разницу между медианой и средним оказывает влияние большое число нулевой стоимости пользователей. Вероятно, это органический трафик.

Самый дорогой трафик - TipTop (средняя стоимость - 2.80 за пользователя), далее идет FaceBoom и AdNonSense примерно в равной ценовой категории (1.11 и 1.01 соответственно). Органический трафик бесплатен, остальные каналы берут за пользователя от 0.21 до 0.72 у.е.

Оцените окупаемость рекламы¶

Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.

  • Проанализируйте окупаемость рекламы c помощью графиков LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проверьте конверсию пользователей и динамику её изменения. То же самое сделайте с удержанием пользователей. Постройте и изучите графики конверсии и удержания.
  • Проанализируйте окупаемость рекламы с разбивкой по устройствам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по странам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по рекламным каналам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Ответьте на такие вопросы:
    • Окупается ли реклама, направленная на привлечение пользователей в целом?
    • Какие устройства, страны и рекламные каналы могут оказывать негативное влияние на окупаемость рекламы?
    • Чем могут быть вызваны проблемы окупаемости?

Напишите вывод, опишите возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.

Для начала оценим общую картину - рассмотрим общую окупаемость рекламы. Для этого построим графики LTV и ROI, а также графики динамики LTV, CAC и ROI с использованием функций get_ltv для расчетов и plot_ltv_roi - для визуализации, а так же def filter_data для сглаживания графиков.

In [38]:
# Исключаем всех "органическсих" пользователей
profiles = profiles.query('channel != "organic"')
In [39]:
# считаем LTV и ROI
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, \
                                                                      observation_date, analysis_horizon)
# строим графики
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon)

Промежуточный вывод:
По графикам можно сделать следующие выводы:

  1. Реклама не окупается к концу 2-ой недели. ROI — около 80%.
  2. CAC нестабилен. Рекламный бюджет увеличивается с течением времени.
  3. На LTV влияет сезонный фактор, но в целом, этот показатель достаточно стабилен. Значит, дело не в ухудшении качества пользователей.
  4. ROI пользователей неумолимо падает. До июля показатель был выше уровня окупаемости, но после стал падать до 0.6.

Окупаемость рекламы с разбивкой по устройствам

In [40]:
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
                                                                      analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)

Комментарий:
Пользователи всех устройств окупались до июня, а потом что-то пошло не так, особенно для владельцев Mac и Iphone. К сентябрю (и какое-то время в августе) ROI пользователей PC на 14 день был на грани окупаемости, ROI всех остальных устройств был уже ниже уровня начиная с июня. LTV пользователей всё так же подвержен сезонности, но стабилен. Стоимость привлечения пользователей PC самая низкая, а ROI - наоборот, выше остальных. В целом, видим, что стоимость привлечения пользователей всех устройств росла за рассматриваемый период.

Направить внимание следует на владельцев Mac и iPhone - стоимость их привлечения высока (и растет от месяца к месяцу), а окупаемоcть страдает.

In [41]:
# посчитаем и визуализируем конверсию, вызвав функции get_conversion() и plot_conversion()
# смотрим конверсию с разбивкой по устройствам

# смотрим конверсию с разбивкой по устройствам
dimensions = ['device']

conversion_raw, conversion_grouped, conversion_history = get_conversion(profiles, orders, observation_date, \
                                                                        analysis_horizon, dimensions=dimensions)

plot_conversion(conversion_grouped, conversion_history, analysis_horizon)

Комментарий:
Все устройства хорошо конверсируются, в лидерах устройства производста компании Apple, из общего потока немного выбиваются привлеченные пользователи, использующие PC: они привлекаются не так удачно.

In [42]:
# смотрим удержание с разбивкой по устройствам

retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, analysis_horizon, dimensions=dimensions
)

plot_retention(retention_grouped, retention_history, analysis_horizon)

Промежуточный вывод:
Не видно, чтобы пользователи каких-то определенных устройств особо плохо удерживались. Неплатящие пользователи удерживаются стабильно плохо, а из платящих лучше всего удерживаются пользователи PC, хуже - владельцы iPhone и Mac.

Окупаемость рекламы с разбивкой по странам

In [43]:
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
                                                                      analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)

Комментарий:

  1. Реклама окупается не везде. В США уровень окупаемости рекламы низкий, а, как мы помним, у нас основная аудитория - это пользователи из США.
  2. При этом стоимость привлечения пользователей из Америки - самая высокая. И она растет.
  3. LTV всё так же подвержен сезонности, но стабилен.
  4. Лучше всего окупается Англия и Германия. Явный аутсайдер - США. Причем окупаемость пользователей начала падать с конца июня 2019 года. Возможно, неудачный релиз, запуск похожего продукта у конкурентов или какие-то изменения в продукте сыграли такую роль, и сервис стал менеее привлекательным для пользователей.

Aкцентируем внимание на пользователей из США.

In [44]:
# смотрим конверсию с разбивкой по странам
dimensions = ['region']

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, analysis_horizon, dimensions=dimensions
)

plot_conversion(conversion_grouped, conversion_history, analysis_horizon)

Комментарий:
Слабая конверсия у пользователей из Англии, Германии и Франции. Но и их доля относительно всех пользователей не такая большая.

In [45]:
# смотрим удержание с разбивкой по странам
retention_raw, retention_grouped, retention_history = get_retention(profiles, visits, observation_date, \
                                                                    analysis_horizon, dimensions=dimensions)

plot_retention(retention_grouped, retention_history, analysis_horizon)

Комментарий:
Видим, что пользователи из США явно плохо удерживаются.

Окупаемость рекламы с разбивкой по рекламным каналам.

In [46]:
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
                                                                      analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)

Комментарий:
Видим большой рост стоимости привлечения пользователей из рекламного канала TipTop. Достаточно много средств было потрачено на эту рекламную сеть,но реальные результаты ниже ожидаемых. Не окупаются пользователи, привлеченные из каналов AdNonSense, FaceBoom, TipTop. Хорошо показала себя рекламная сеть YRabbit в июле - ROI пользователей на 14 день был высок.

In [47]:
# смотрим конверсию с разбивкой по каналам привлечения
dimensions = ['channel']

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles, orders, observation_date, analysis_horizon, dimensions=dimensions
)

plot_conversion(conversion_grouped, conversion_history, analysis_horizon)

Комментарий:
Выше всего конверсия у пользователей, привлеченных посредством FaceBoom. Но вообще, динамики у всех пользователей в течение двухнедельного лайфтайма практически и нет.

In [48]:
# смотрим удержание с разбивкой по каналам привлечения

retention_raw, retention_grouped, retention_history = get_retention(
    profiles, visits, observation_date, analysis_horizon, dimensions=dimensions
)

plot_retention(retention_grouped, retention_history, analysis_horizon)

Промежуточный вывод:
Хуже всего из платящих удерживаются пользователи из FaceBoom и AdNonSense.

Итак, в результате анализа окупаемости рекламы можно сделать следующие выводы:

  1. в общем и целом реклама не окупается;
  2. это вызвано низкой окупаемостью рекламы в США, как основной страны присутствия;
  3. пользователи из США конверсируются больше, но есть проблемы с удержанием платящих пользователей;
  4. один из самых проблемных каналов - FaceBoom (не окупается имея второе место по сумме затрат на рекламу, низкое удержание платящих пользователей); далее хотим отметить TipTop - неоправданно высокий рост САС, в следствие чего он через месяц перестал окупаться;
  5. по устройствам в аутсайдерах конверсии пользователи PC, но при этом они неплохо удерживаются.

Напишите выводы¶

  • Выделите причины неэффективности привлечения пользователей.
  • Сформулируйте рекомендации для отдела маркетинга.

Вывод и рекомендация:

Компания-заказчик представлена на рынке Европы (Германия, Франция и Великобритания) и в США. Несмотря на огромные вложения в рекламу, последние несколько месяцев компания терпит убытки.

Мы проанализировали данные, предоставленные компанией-заказчиком (информация о пользователях, привлечённых с 1 мая по 27 октября 2019 года):

  • лог сервера с данными об их посещениях,
  • выгрузку покупок за этот период,
  • рекламные расходы.

В результате исследования было выявлено, что основной причиной финансовых проблем являются рекламные траты на привлечение пользователей посредством FaceBoom, TipTop в США и AdNonSence в Европе (топ3 САС):

  • высокая стоимость привлечения у этих каналов, с горизонтом событий в две недели она не окупается;

  • при этом, платящие пользователи FaceBoom и AdNonSence очень плохо удерживаются;

  • рекламная система TipTop оказалась неэффективной, расходы на привлечение в TipTop за полгода выросли почти в три раза;

  • пользователи Mac и IPhone оказались самыми неплатящими, возможно есть проблема с оптимизацией приложения Procrastinate Pro+ для ios устройств;

  • и летом 2019 произошло какое-то событие в США, которое кардинально повлияло на поведение пользователей.

Рекомендация

  1. Регион United States - основной рынок для приложения. Следует пересмотреть подходы к работе с рекламными источниками TipTop и FaceBoom, возможно их получится оптимизировать и улучшить качество привлекаемых пользователей;
  2. В Европе также стоит пересмотреть траты на рекламный источник AdNonSence т.к. удержание платящих пользователей сильно ниже, чем у остальных источников трафика. Во Франции с 2019-08 по 2019-10 есть проблемы с удержанием на 14-й день (судя по прерывистым линиям на графике динамики удержания пользователей на 14-й день). В Германии стоит обратить внимание на Retention, поскольку много данных отсутсвуют. Если данных нет - значит пользователи не доходят до 14-дня ретеншена.
  3. Поработать с органическими пользователями: это существенная доля дохода приложения;
  4. Привлекать больше PC и Android пользователей.
  5. Рассмотреть возможность перераспределения рекламного бюджета с неэффективных источников на MediaTornado, RocketSuperAds, YRabbit.

Дополнительные ссылки

https://blog.ohmystats.com/cohort-analysis/

https://gopractice.ru/cohort_analysis/

https://smysl.io/blog/pandas-cohorts/